""" sbak/ui.py - sbak.ui - Virtual user input.

    WARNING!!! This module is littered with TODOs (i.e. unimplemented
    features)
    
    Pygame provides access to information about the physical hardware
    that is attached to the computer as well as a series of events that
    occur whenever something happens to the hardware, such as whenever
    a key is pressed or released, or whenever the mouse moves. Pygame
    supports three kinds of hardware input: Keyboard, mouse, and
    joystick. This module provides a fourth kind of input which is
    simply referred to as "UI".
    
    UI inputs are not mapped to a single hardware device. Instead,
    UI's are programmed to respond to other kinds of input, either
    keyboard, mouse, or joystick, in various different ways. Basically,
    UI's wrap around one or more other kinds of input.
    
    The key components to this module are UI classes. There are (will be)
    several types of UI classes, with the actual `UI` class serving as
    an abstract base for all the others. The others do most of the real
    work in responding to hardware events to change their state and
    generate their own events.
    
    ~~~ Types of UI ~~~
    
    The following is a brief summary of the different UI classes:
    
    Button:     A boolean 'up' or 'down' input, comparible to a
                keyboard key or mouse button.

    ~~~ UI Events ~~~
    
    When the state of a UI object changes, it posts a UIEVENT event on
    the Pygame event queue. A UIEVENT event has the following
    attributes:
    
    ui:         The UI object that issued the event.
    
    id:         The ID of the UI object that issued the event. This is
                the same as ui.id. (This event attribute exists so that
                users can hook handlers to UIEVENT events associated
                with a particular ui ID rather than a ui object.)
    
    uitype:     This will be one of the sbak.event.UI_* constants and
                corresponds to the class of the UI object that caused
                the UIEVENT event to occur. Other attributes may be
                available on the event depending on the value of
                uitype:

    TODO: ??? Make UI a Resource type so configs can be saved?
    
    TODO: This module was NOT properly updated for use with the most
    recent event system. It will work, but it also NEEDS work.
"""

# ====== GLOBAL IMPORTS ======

# Quick fix: Apparently, Python 2.5 doesn't support copysign...
try:
    from math import copysign
except ImportError:
    def copysign(x, y):
        """ Substitute for math.copysign.
        
            x = copysign(x, y)
        
            This is used by the `sbak.ui` module when `math.copysign`
            does not exist. It returns a float version of x with the
            sign of y, and assumes the sign is 1 when y is 0.
        """
        return float(abs(x) * (-1 if y < 0 else 1))
import operator
import weakref
import pygame
from pygame import (
    KEYUP,
    KEYDOWN,
    MOUSEMOTION,
    MOUSEBUTTONUP,
    MOUSEBUTTONDOWN,
    JOYBUTTONUP,
    JOYBUTTONDOWN,
    JOYAXISMOTION,
    JOYBALLMOTION,
    JOYHATMOTION
)


# ====== LOCAL IMPORTS ======

from sbak.geometry import FPoint
from sbak.event import (
    UIEVENT,
    UI_BUTTONUP,
    UI_BUTTONDOWN,
    get_key_num,
    get_key_name,
    post as event_post,
    Event
)


# ====== GLOBALS ======

__all__ = (
    'init',
    'UI',
    'Button',
    'get'
)

_init = False


# ====== CLASSESS ======

class UI(object):
    """ Abstract base UI.
    
        This class defines basic behavior that is shared by all UI
        classes, but does not function as a stand-alone class.
        
        Subclasses of UI should implement event handlers for every
        type of event that they plan on responding to.
        Each instance of the subclasses should define a mapping that
        maps event types to the corresponding handlers. This mapping
        should then be assigned to the `self.handlers` attribute,
        which the UI constructor defines as an empty dict by default.
        After this assignment, the self.hook_handlers() method can be
        called to hook all handlers to an event manager. (The UI
        base constructor does not call this itself.)
        
        ~~~ UI Constructor Arguments ~~~
        
        The following keyword arguments are accepted by all UI
        classes:
        
        <none>
        
        ~~~ UI Members ~~~
        
        These attributes are common to all instances of all proper
        subclasses of UI.
        
        id:     The unique identifier for the UI object. This can be
                used to retrieve a UI object using the sbak.ui.get(...)
        function assuming at least one strong reference to it exists
        outside the global UI registry.
    
        handlers:   A mapping of event type constants to event handlers
                    for the UI object. Adding items to this will not
        automatically hook the handlers; you must call
        self.hook_handlers(...) after modifying this mapping before the
        handlers will actually be hooked to event types.
        
        hooks:      A mapping of event type constants to hooks of
                    handlers. After calling self.hook_handlers(...),
        this mapping should have the same keys as self.handlers. If
        you want to manually remove hooks in this member, make sure
        to call the .remove() method for each hook, then delete the
        hook from the mapping, or else errors will occur when
        self.hook_handlers(...) or self.remove_handlers() are called.
        
        
        
        TODO: ??? Make UI an event Responder and ditch the .hooks/handlers attrs ???
    """
    
    def __init__(self, id=None, events=None, **kwargs):
        """ The constructor for the base UI class.
        
            This should be called once by subclass constructors
            before accessing any other UI members and before
            assigning to the `self.handlers` attribute. In most cases
            simply passing the same arguments that were passed to the
            subclass's constructor along to this base constructor will
            work.
        """
        self._id = id
        _register(self)
        self.handlers = {} # Maps event types to handlers.
        self.hooks = {} # Maps event types to hooks.
    
    id = property(
        fget = operator.attrgetter('_id'),
        doc =
        """ The unique identifier of the UI object.
        
            Each UI must have a unique ID. This ID can be used to
            retrieve a UI object after it has been created by using
            the `sbak.ui.get()` function. However, the UI must be
            strongly referenced somewhere or else it will not stay
            in the registry.
        
            Read only.
        """
    )
    
    def hook_handlers(self, events = None):
        """ Hooks all handlers to an event manager.
        
            events: The event manager to hook handlers to. If None is
                    passed, the UI will attempt to use the event
                    manager of the default game. (In other words, it
                    will use `sbak.game.default.events`.)
            
            All existing hooks that the UI created will be removed
            before the new ones are created.
        """
        if events is None:
            from sbak import game
            events = game.default.events
            
        self.remove_handlers()
        hooks = self.hooks
        for type, hook in self.handlers.iteritems():
            hooks[type] = events.hook(hook, type)
    
    def remove_handlers(self):
        """ Removes all hooks that the UI created. """
        hooks = self.hooks
        for type in hooks.keys():
            hooks[type].unhook()
            del hooks[type]
    
    def __call__(self):
        """ Returns the state of the input.
        
            This is not implemented for the base UI class and will
            raise NotImplementedError. Subclasses of UI should
            implement this to return the value of the member that
            corresponds to the input state for the UI object.
        """
        raise NotImplementedError("UI base class does not implement calling.")

class Button(UI):
    """ A boolean up- or down-only UI.
    
        A Button UI is comparible to a keyboard key or mouse button. It
        has a boolean state, meaning it can be either "pressed" or
        "released." A separate event is posted for a change to each.
        
        The pressed state of the button can be accesed by calling the
        button or by checking the value of the .pressed member. The
        Button's non-zero-ness (truth) is also determined by the
        pressed state.
    
        ~~~ Button Accepted Arguments ~~~
        
        In addition to the arguments accepted by the base UI
        constructor, the Button constructor also accepts the following
        keyword arguments:
        
        key:
            Keycode or named keyboard key. The button will be pressed
            when this key is pressed and released when the key is
            released.
        
        keys:
            Plural version of 'key'. Pass a sequence of keycodes and/or
            key names. If any key is pressed, the button is pressed.
            If any key is released, the button is released.
        
        mouse_button:
            Mouse button number. If this mouse button is pressed, the
            Button is pressed. If the mouse button is released, the
            Button is released.
        
        mouse_buttons:
            Plural version of 'mouse_button'. Pass a sequence of mouse
            button numbers. If any mouse button is pressed, the Button
            is pressed. If any mouse button is released, the Button is
            released.
        
        joy_button:
            Joystick button identifier. This must be a pair (two-value
            sequence) containing two numbers. The first is a joystick
            ID, the second is the number of a button on the joystick
            with the given ID. When the joystick button is pressed,
            the Button is pressed. When the joystick button is
            released, the Button is released.
        
        joy_buttons:
            Plural version of 'joy_button'. Pass a sequence of one or
            more pairs like the one passed to joy_button. If any
            joystick button is pressed, the Button is pressed. If any
            joystick button is released, the Button is released.
        
        joy_axis_threshold:
            A joystick axis threshold. Pass a numeric triple (a
            sequence of 3 number). The first is the ID of a joystick,
            the second is the number of an axis on the joystick, and
            the third is a "threshold" value, a float in the range
            -1.0 to 1.0. (This is the complete range for a joystick
            axis.) When the indicated joystick axis moves in the
            direction indicated by the sign of the treshold beyond the
            threshold value, the Button is pressed. When the axis moves
            before the threshold, the Button is released.
        
        joy_axis_thresholds:
            Plural version of 'joy_axis_threshold'. Pass a sequence of
            one or more triples like the ones passed to
            joy_axis_treshold. If any axis moves beyond the threshold
            of the corresponding axis, the Button is pressed. If any
            axis moves behind the threshold for the corresponding axis,
            the Button is released. You can also pass a mapping where
            the keys are (JOY, AXIS) pairs and the values are tresholds
            corresponding to the indicated joystick axis.
            Note that only one trehshold can exist for each axis,
            regardless of whether you use a sequence or a mapping.
        
        TODO: arguments for joy hats
        
        ~~~ Button Instance Members ~~~
        
        The members below give direct access to the information used by
        the Button object to determine when and how it should respond
        to hardware input events. It is not recommended that you use
        them directly because no error checking will be performed on
        values assigned to them. Unless you are an advanced user, it
        would be better if you used the methods of Button that
        correspond to each of these attributes.
        
        keys:
            A set containing the keycodes of all keys that the button
            responds to. Add key codes to make the button respond to
            more keys, or remove key codes to make it stop responding
            to keys. You can also use the add_key() and remove_key()
            methods to add or remove keys by name, or get_named_keys()
            to return a set containing the names of keys that the
            Button is responding to (rather than the keycodes).
        
        mouse_buttons:
            A set containing the numbers of all mouse buttons that the
            Button responds to. Add integers to it to make the Button
            to respond to new mouse buttons or remove numbers from it
            to make it stop responding to mouse buttons. You can use
            the add_mouse_button() and remove_mouse_button() methods to
            add or remove buttons from this set so that value checking
            and conversion is performed for each button number.
        
        joy_buttons:
            A set containing all the (JOYID, BUTTON) pairs that the
            Button is responding to. Add pairs to make the Button
            respond to more joystick buttons, remove pairs to make it
            respond to fewer. Use the joy_add_button() and
            joy_remove_button() methods to add or remove pairs while
            performing the necessary value type checking and
            conversion..
        
        joy_axis_tresholds:
            A dictionary that maps (JOYID, AXIS) pairs to the threshold
            values of the indicated joystick axis. Each key must be
            a pair and each value must be a threshold. This MUST be a
            dict or similar type of object. Use
            joy_add_axis_treshold() and joy_remove_axis_threshold() to
            add or remove axis tresholds while performing the necessary
            type checking and conversion.
            
        joy_hat_pos:
            TODO: DOCUMENT ME! (works similarly to joy_axis_threshold)
        
        joy_hats_pos:
            TODO: DOCUMENT ME! (works similarly to joy_axis_thresholds)
        
        joy_hat_x:
            TODO: DOCUMENT ME!
        
        joy_hats_x:
            TODO: DOCUMENT ME!
            
        joy_hat_y:
            TODO: DOCUMENT ME!
        
        joy_hats_y:
            TODO: DOCUMENT ME!
        
        
        The following members are considered "suitable" for direct
        assignment by all users:
        
        pressed:
            A boolean flag that indicates whether or not the Button is
            currently pressed. Changing the value will simulate a
            Button press or release, just as if one of the hardware
            inputs had been pressed.
        
        ~~~ Button UI Events ~~~
        
        Each Button object can post one of two kinds of UIEVENT
        events to the Pygame event queue:
        
        * A UIEVENT with a `uitype` value of UI_BUTTONDOWN is posted
          when the Button is pressed. This happens when the relavent
          keyboard keys, mouse buttons, or joystick buttons are pressed,
          etc.
        
        * A UIEVENT with `uitype` value of UI_BUTTONUP is posted
          when the Button is released. This happens when the relavent
          keys, mouse buttons, or joystick buttons are released, etc.
        
        The attributes for either of the two event types are the same:
        
        type:   Always UIEVENT.
        uitype: UI_BUTTONDOWN or UI_BUTTONUP.
        ui:     The Button object that caused the event.
        id:     The ID of the Button object.
    """
    
    def __init__(self, id=None, events=None, **kwargs):
        super(Button,self).__init__(id, events)
        
        # Set defaults
        self._pressed = False
        self.keys = set()
        self.mouse_buttons = set()
        self.joy_buttons = set()
        self.joy_axis_thresholds = {}
        self.joy_hats_pos = {}
        self.joy_hats_x = {}
        self.joy_hats_y = {}
        
        # Set handlers
        self.handlers = {
            KEYUP : self.handle_keyup,
            KEYDOWN : self.handle_keydown,
            MOUSEBUTTONUP : self.handle_mousebuttonup,
            MOUSEBUTTONDOWN : self.handle_mousebuttondown,
            JOYBUTTONUP : self.handle_joybuttondown,
            JOYBUTTONDOWN : self.handle_joybuttondown,
            JOYAXISMOTION : self.handle_joyaxismotion,
            JOYHATMOTION : self.handle_joyhatmotion
        }
        
        # Hook handlers
        self.hook_handlers(events)
        
        # Allowing input types:
        
        # Allow keyboard keys:
        keys = kwargs['keys'] if 'keys' in kwargs else set()
        if 'key' in kwargs:
            keys = set(keys)
            keys.add(kwargs['key'])
        self.add_key(*keys)
            
        # Allow mouse buttons:
        btns = kwargs['mouse_buttons'] if 'mouse_buttons' in kwargs else set()
        if 'mouse_button' in kwargs:
            btns = set(btns)
            btns.add(kwargs['mouse_button'])
        self.add_mouse_button(*btns)
        
        # Allows joystick buttons:
        btns = kwargs['joy_buttons'] if 'joy_buttons' in kwargs else set()
        if 'joy_button' in kwargs:
            btns = set(btns)
            btns.add(kwargs['joy_button'])
        self.add_joy_button(*btns)
        
        
        # Allow joystick axis thresholds:
        axes = kwargs['joy_axis_thresholds']\
                if 'joy_axis_thresholds' in kwargs else set()
        
            # For dict threshold tables:
        if hasattr(axes, 'iteritems'):
            if 'joy_axis_threshold' in kwargs:
                t = tuple(kwargs['joy_axis_threshold'])
                axes[t[0:2]] = t[2]
            self.add_joy_axis_threshold(axes)
        
            # For set threshold tables:
        else:
            if 'joy_axis_threshold' in kwargs:
                axes = set(axes)
                axes.add(kwargs['joy_axis_threshold'])
            self.add_joy_axis_threshold(*axes)
        
        
        # Allow joystick hat positions:
        hatspos = kwargs['joy_hats_pos'] if 'joy_hats_pos' in kwargs else set()
        
            # For dict hat positions:
        if hasattr(hatspos,'iteritems'):
            if 'joy_hat_pos' in kwargs:
                p = tuple(kwargs['joy_hat_pos'])
                hatspos[p[0:2]] = p[2]
            self.add_joy_pos(hatspos)
            
            # For set hat positions:
        else:
            if 'joy_hat_pos' in kwargs:
                hatspos = set(hatspos)
                hatspos.add(tuple(kwargs['joy_hat_pos']))
            self.add_joy_hat_pos(*hatspos)
    
    
        # Allow joystick hat x positions:
        hatsx = kwargs['joy_hats_x'] if 'joy_hats_x' in kwargs else set()
        
            # For dict hat positions:
        if hasattr(hatspos,'iteritems'):
            if 'joy_hat_x' in kwargs:
                p = tuple(kwargs['joy_hat_x'])
                hatsx[p[0:2]] = p[2]
            self.add_joy_hat_x(hatsx)
            
            # For set hat positions:
        else:
            if 'joy_hat_x' in kwargs:
                hatsx = set(hatsx)
                hatsx.add(tuple(kwargs['joy_hat_x']))
            self.add_joy_hat_x(*hatsx)
        
        
        # Allow joystick hat y positions:
        hatsy = kwargs['joy_hats_y'] if 'joy_hats_y' in kwargs else set()
        
            # For dict hat positions:
        if hasattr(hatspos,'iteritems'):
            if 'joy_hat_y' in kwargs:
                p = tuple(kwargs['joy_hat_y'])
                hatsy[p[0:2]] = p[2]
            self.add_joy_hat_y(hatsy)
            
            # For set hat positions:
        else:
            if 'joy_hat_y' in kwargs:
                hatsy = set(hatsy)
                hatsy.add(tuple(kwargs['joy_hat_y']))
            self.add_joy_hat_y(*hatsy)
    
    # Responses:
    
    def press(self):
        """ Simulates a button press.
        
            This will cause the state of the Button to change to True,
            and will post a properly-attributed UI_BUTTONDOWN event to
            the Pygame event queue.
        """
        self._pressed = True
        event_post(Event(
            UIEVENT,
            uitype = UI_BUTTONDOWN,
            ui = self,
            id = self.id
        ))
    
    def release(self):
        """ Simulates a button release.
        
            This will cause the state of the Button to change to False,
            and will post a properly-attributed UI_BUTTONUP event to
            the Pygame event queue.
        """
        self._pressed = False
        event_post(Event(
            UIEVENT,
            uitype = UI_BUTTONUP,
            ui = self,
            id = self.id
        ))
        
    def _set_pressed(self, p):
        self.press() if p else self.release()
    
    pressed = property(
        fget = operator.attrgetter('_pressed'),
        fset = _set_pressed,
        doc =
        """ Flag indicating whether or not the button is pressed.
        
            This will always be either a boolean True or False,
            regardless of what value is set to it.
        
            Changing the value of this property will invoke either
            Button.press() or Button.release() depending on the
            truth of the value given.
        """
    )
    
    __call__ = __nonzero__ = __bool__ = pressed.fget
    
    # Keyboard response methods:
    
    def add_key(self, *keys):
        """ Adds one or more responder keys to the Button.
        
            >>> button.add_key( KEY, KEY, ... )
        
            Pass one or more keycodes and/or key names. Keycodes must
            be convertable to int, key names must be of type str or
            unicode. Values that cannot be converted to ints are assumed
            to be strings (i.e. key names).
            
            Raises TypeError if a keycode cannot be converted to
            int and is not a string type.
            
            Raises ValueError if a named key is unrecognized.
        """
        for k in keys:
            try:
                k = int(k)
            except (ValueError, TypeError):
                k = get_key_num(k)
            
            self.keys.add(k)
    
    def remove_key(self, *keys):
        """ Removes one or more responder keys from the Button.
        
            >>> button.remove_key( KEY, KEY, ... )
        
            Pass one or more keycodes and/or key names. Keycodes must
            be convertable to int, key names must be of type str or
            unicode. Values that cannot be converted to ints are assumed
            to be strings (i.e. key names).
            
            Duplicate keys are ignored.
            
            Raises TypeError if a keycode cannot be converted to
            int and is not a string type.
            
            Raises ValueError if a named key is unrecognized.
        """
        keys = set(keys)
        for k in keys:
            try:
                k = int(k)
            except (ValueError, TypeError):
                k = get_key_num(k)
                
            self.keys.remove(k)
    
    def get_named_keys(self):
        """ Get names of keys that the Button is responding to.
        
            This returns a set containing the names (as strings) of the
            keys that the Button is responding to.
            
            Returns an empty set if the Button is not responding to any
            keys.
            
            Note that changing the contents of the set returned will
            not effect the Button in any way. To add or remove keys to
            or from the Button, use the add_key and remove_key methods.
        """
        return set(get_key_name(k) for k in self.keys)
    
    # Mouse response methods:
    
    def add_mouse_button(self, *buttons):
        """ Add response for one or more mouse buttons.
        
            >>> button.add_mouse_button( BUTTON, BUTTON, ... )
            
            Pass one or more button ID numbers. Each value must be
            convertable to string.
            
            Raises TypeError or ValueError if a value cannot be
            converted to int.
        """
        for b in buttons:
            b = int(b)
            self.mouse_buttons.add(b)
    
    def remove_mouse_button(self, *vargs):
        """ Add response for one or more mouse buttons.
        
            >>> button.add_mouse_button( BUTTON, BUTTON, ... )
            
            Pass one or more button ID numbers. Each value must be
            convertable to string.
            
            Duplicate buttons are ignored.
            
            Raises TypeError or ValueError if a value cannot be
            converted to int.
        """
        buttons = set(buttons)
        for b in buttons:
            b = int(b)
            self.mouse_buttons.add(b)
    
    # Joystick response methods:
    
    def add_joy_button(self, *pairs):
        """ Add response for one or more joystick buttons.
        
            >>> button.add_joy_button( (JID, BUTTON), ...)
            
            Each argument passed must be a sequence of 2 values. Each
            item in each sequence must be convertable to type `int`.
            Not meeting these requirements will cause a TypeError or
            ValueError to occur.
            
            Examples:
            
            >>> from sbak.ui import Button
            >>>
            >>> # Create a generic button.
            >>> b = Button('action', ...)
            >>>
            >>> # Add responders for button 0 on joystick 0,
            >>> # and button 0 on joystick 1.
            >>> b.add_joy_button( (0,0), (1,0) )
        """
        for joy, btn in pairs:
            pair = joy, btn = int(joy), int(btn)
            init_joy(joy)
            self.joy_buttons.add(pair)
    
    def remove_joy_button(self, *pairs):
        """ Remove response for one or more joystick buttons.
        
            >>> button.add_joy_button( (JID, BUTTON), ...)
            
            Each argument passed must be a pair (sequence of 2 values).
            Each item in each pair must be convertable to type `int`.
            Not meeting these requirements will cause a TypeError or
            ValueError to occur.
            
            Duplicate pairs are ignored, but a KeyError may be raised
            if an attempt it made to remove a pair that is not part of
            the Button.
        """
        pairs = set(pairs)
        for joy, btn in pairs:
            pair = int(joy), int(btn)
            self.joy_buttons.remove(pair)
    
    def add_joy_axis_threshold(self, *vargs):
        """ Add/change response threshold for a joystick axis.
        
            >>> button.add_joy_axis_threshold((JID, AXIS, T), ...)
            >>> button.add_joy_axis_threshold({(JID, AXIS) : T, ...})
        
            This method has two calling conventions, but both are used
            to do the same thing: add a response threshold for a
            joystick axis, or change an existing threshold.
            
            Convention 1:   Pass a one or more numeric triples. The
            first value of each triple, JID, is the ID of a joystick.
            The second value, AXIS, is the number of an axis on the
            joystick. The last value, T, is a "threshold" value. This
            is a float in the range -1.0 to 1.0 that indicates how far
            the axis must be pushed and in what direction in order for
            the Button to be pressed. If a T is positive, the axis must
            rise above the value, and if the T is negative, the axis
            must sink below it.
            
            Convention 2:   Instead of passing multiple numeric triples
            as variable argument, pass a single mapping. This mapping
            maps (JID, AXIS) pairs to T values. This convention matches
            more closely how axis thresholds are actually stored inside
            the Button object.
            
            Each JID and AXIS must be convertable to int, and each T
            must be convertable to float. Only one T can exist for every
            JID,AXIS pair. If more than one T is given for a JID,AXIS
            pair, only the most recently added one is used.
            
            TypeError or ValueError may be raised if a any value cannot
            be converted to the needed type/value.
        """
        if not vargs:
            return
        
        first = vargs[0]
        
        if hasattr(first, 'iteritems'):
            if len(vargs) > 1:
                raise TypeError("Expected 1 argument (a mapping), got %i"
                                % len(vargs))
            
            for pair, th in first.iteritems():
                joy, axis = map(int, pair)
                init_joy(joy)
                th = float(th)
                self.joy_axis_thresholds[(joy, axis)] = th
        
        else:
            for joy, axis, th in vargs:
                joy, axis = int(joy), int(axis)
                th = float(th)
                self.joy_axis_thresholds[(joy, axis)] = th
    
    def remove_joy_axis_threshold(self, *pairs):
        """ Removes response threshold for a joystick axis.
        
            >>> button.remove_joy_axis_threshold( (JID,AXIS), ... )
            
            Pass one or more (JID,AXIS) pairs. The threshold for each
            will be removed from self.joy_axis_thresholds.
            
            Each JID and AXIS must be convertable to int. If not,
            either TypeError or ValueError will be raised.
            
            Duplicate pairs are ignored, but a KeyError may be raised
            if an attempt it made to remove a pair that is not part of
            the Button.
        """
        pairs = set(pairs)
        for pair in pairs:
            pair = tuple(map(int, pair))
            del self.joy_axis_thresholds[pair]
    
    def add_joy_hat_pos(self, *triples):
        """ TODO: DOCUMENT ME!
        
            NOTE: Works similarly to add_joy_axis_threshold, only there
            are position (x,y) pairs instead of thresholds.
        """
        if not triples:
            return
        
        first = triples[0]
        
        if hasattr(first, 'iteritems'):
            for pair, pos in first.iteritems():
                (joy, hat), (x,y) = map(int, pair), map(int, pos)
                init_joy(joy)
                self.joy_hats_pos[(joy,hat)] = (x,y)
        
        for joy, hat, pos in triples:
            (joy, hat), (x,y) = map(int, (joy,hat)), map(int, pos)
            init_joy(joy)
            self.joy_hats_pos[(joy,hat)] = (x,y)
    
    def remove_joy_hat_pos(self, *pairs):
        """ TODO: DOCUMENT ME! """
        pairs = set(pairs)
        for pair in pairs:
            pair = tuple(map(int, pair))
            del self.joy_hats_pos[pair]
    
    def add_joy_hat_x(self, *triples):
        """ TODO: DOCUMENT ME!
        
            NOTE: Works similarly to add_joy_axis_threshold, only there
            are x positions instead of thresholds.
        """
        if not triples:
            return
        
        first = triples[0]
        
        if hasattr(first, 'iteritems'):
            for pair, x in first.iteritems():
                (joy, hat), x = map(int, pair), int(x)
                init_joy(joy)
                self.joy_hats_x[(joy,hat)] = x
        
        for joy, hat, x in triples:
            (joy, hat), x = map(int, (joy,hat)), int(x)
            init_joy(joy)
            self.joy_hats_x[(joy,hat)] = x
    
    def remove_joy_hat_x(self, *pairs):
        """ TODO: DOCUMENT ME! """
        pairs = set(pairs)
        for pair in pairs:
            pair = tuple(map(int, pair))
            del self.joy_hats_x[pair]
    
    def add_joy_hat_y(self, *triples):
        """ TODO: DOCUMENT ME!
        
            NOTE: Works similarly to add_joy_axis_threshold, only there
            are y positions instead of thresholds.
        """
        if not triples:
            return
        
        first = triples[0]
        
        if hasattr(first, 'iteritems'):
            for pair, y in first.iteritems():
                (joy, hat), y = map(int, pair), int(y)
                init_joy(joy)
                self.joy_hats_y[(joy,hat)] = y
        
        for joy, hat, y in triples:
            (joy, hat), y = map(int, (joy,hat)), int(y)
            init_joy(joy)
            self.joy_hats_y[(joy,hat)] = y
    
    def remove_joy_hat_y(self, *pairs):
        """ TODO: DOCUMENT ME! """
        pairs = set(pairs)
        for pair in pairs:
            pair = tuple(map(int, pair))
            del self.joy_hats_y[pair]
    
    # Event handlers:
    
    def handle_keydown(self, e):
        """ Button's KEYDOWN handler.
        
            If the key is one that the Button is set to respond to,
            this changes state to True and issues a
            UIEVENT/UI_BUTTONDOWN event, else does nothing.
        """
        if e.key in self.keys:
            self.press()
    
    def handle_keyup(self, e):
        """ Button's KEYUP handler.
        
            If the key is one that the Button is set to respond to,
            this changes state to False and issues a
            UIEVENT/UI_BUTTONUP event, else does nothing.
        """
        if e.key in self.keys:
            self.release()
    
    def handle_mousebuttondown(self, e):
        """ Button's MOUSEBUTTONDOWN handler.
        
            If the mouse button is one that the Button is set to
            respond to, this changes state to True and issues a
            UIEVENT/UI_BUTTONDOWN event, else does nothing.
        """
        if e.button in self.mouse_buttons:
            self.press()
    
    def handle_mousebuttonup(self, e):
        """ Button's MOUSEBUTTONUP handler.
        
            If the mouse button is one that the Button is set to
            respond to, this changes state to False and issues a
            UIEVENT/UI_BUTTONUP event, else does nothing.
        """
        if e.button in self.mouse_buttons:
            self.release()
    
    def handle_joybuttondown(self, e):
        """ Buttons' JOYBUTTONDOWN handler.
        
            If the (joystick, button) pair (indicated by e.joy and
            e.button) is one that the Button is configured to
            respond to, the Button will be pressed, and a
            UIEVENT/UI_BUTTONDOWN evnet will be posted.
        """
        pair = (e.joy, e.button)
        if pair in self.joy_buttons:
            self.press()
    
    def handle_joybuttonup(self, e):
        """ Buttons' JOYBUTTONDOWN handler.
        
            If the (joystick, button) pair (indicated by e.joy and
            e.button) is one that the Button is configured to
            respond to, the Button will be pressed, and a
            UIEVENT/UI_BUTTONDOWN evnet will be posted.
        """
        pair = (e.joy, e.button)
        if pair in self.joy_buttons:
            self.release()
    
    def handle_joyaxismotion(self, e):
        """ Button's JOYAXISMOTION handler.
        
            Responds to axis motion by changing state and posting
            events whenever a responsive axis moves through a
            threshold value. See the main Button documentation for
            details.
        """
        # FIXME: There MUST be a way to simplify these checks...
        
        pair = (e.joy, e.axis)
        val = e.value
        
        if pair in self.joy_axis_thresholds:
            
            th = self.joy_axis_thresholds[pair]
            
            if (((th > 0 and val >= th)
                 or (th < 0 and val <= th)
                 or (th == 0 and val == 0))
                and not self.pressed):
                self.press()
                
            elif (((th > 0 and val < th)
                  or (th < 0 and val > th)
                  or (th == 0 and val != 0))
                  and self.pressed):
                self.release()
    
    def handle_joyhatmotion(self, e):
        """ TODO: DOCUMENT ME! """
        
        hatpos, hatx, haty = self.joy_hats_pos, self.joy_hats_x, self.joy_hats_y
        
        pair = e.joy, e.hat
        pos = x, y = e.value
        
        if not self.pressed:
            if ((hatpos and pair in hatpos and hatpos[pair] == pos)
                or (hatx and pair in hatx and hatx[pair] == x)
                or (haty and pair in haty and haty[pair] == y)):
                
                self.press()
        
        else:
            if ((hatpos and pair in hatpos and hatpos[pair] != pos)
                or (hatx and pair in hatx and hatx[pair] != x)
                or (haty and pair in haty and haty[pair] != y)):
                
                self.release()


# ====== FUNCTIONS ======

def init():
    """ Initializes the UserInput system.
        This should be called before using any other part of this module.
        Calling this function multiple times has no effect so it is same to
        call it more than once.
    """
    global _inp, _init, _joysticks
    if not _init:
        _init = True
        
        # Initialize the Joystick module.
        pygame.joystick.init()
        
        # Create the UI registry
        _inp = weakref.WeakValueDictionary()
        
        # This is a registry of Joystick objects used by all UI's. It maps joystick
        # ID's to Joystick objects. When a request is made to make use of a
        # joystick that is not in this registry, the joystick is initialized and
        # added to the registry.
        _joysticks = {}

def get(ID):
    """ Returns the UI with the given ID. """
    return _inp[ID]

def _register(uiObj):
    ID = uiObj.id
    if ID in _inp:
        raise ValueError("UI with id=='%s' already exists."%ID)
    _inp[ID] = uiObj

def _unregister(uiObj):
    ID = uiObj.id
    del _inp[ID] # KeyError will be raised for ui's not registered

def init_joy(*joys):
    """ Initialize one or more joysticks.
    
        Pygame requires that each joystick must be initialized before
        events will occur in response to it. This function can be used
        to initialize one or more joysticks by passing their numeric
        (integer) IDs so that UI objects can respond to them.
        
        You don't normally need to call this function yourself when
        using UI classes; they will call it as necessary themselves.
        
        This function suppresses errors generated by Pygame about
        incorrect joystick ID numbers, etc. so it is fairly
        transparent. If you need more control over joysticks, use the
        pygame.joystick module directly.
    """
    global _joysticks
    for j in joys:
        if j not in _joysticks:
            try:
                jobj = pygame.joystick.Joystick(j)
                jobj.init()
                _joysticks[j] = jobj
            except pygame.error:
                pass
